local MODNAME, DailyIntake = ...

-------------------------------------------------------------------------------
-- Localized Lua globals
-------------------------------------------------------------------------------

local _G = getfenv(0)

local abs		= _G.abs
local bit		= _G.bit
local ipairs	= _G.ipairs
local pairs		= _G.pairs
local select	= _G.select
local sqrt		= _G.sqrt
local tonumber	= _G.tonumber
local wipe		= _G.wipe

-------------------------------------------------------------------------------
-- AddOn namespace
-------------------------------------------------------------------------------


-- Defines common quest item priority numbers. These are necessary when more than one item may be used
-- in certain conditions. The item with the highest priority will be chosen.
local Priority =
{
    Lowest = 0,
    Lower = 5000,
    Low = 10000,
    Normal = 15000,
    High = 20000,
    Higher = 25000,
    Highest = 30000;
};

-- Defines entity types to use with UnitIsEntity function.
local Entity =
{
    Player = 0x000,
    Mob = 0x003,
    Pet = 0x004,
    Vehicle = 0x005
};

-- Contains the list of currently available quest items.
local availableQuestItems = {};

-- Contains the list of all registered quest items.
local QuestItemList = {};

-- frame that receives events
local frame = CreateFrame("Frame");
local events = {};

-- a flag that alows to temporarely stop macro refresh
local isMacroRefreshEnabled = true;

--[[
Initializes the QuestItems feature.
--]]
local function Initialize()
   -- setup events
    frame:SetScript("OnEvent", function(self, event, ...)
        events[event](self, ...); -- call one of the functions above
    end);

    frame:SetScript("OnUpdate", function(self, ...)
        events.TIMER(self, ...); -- call one of the functions above
    end);

    for eventName, _ in pairs(events) do
        if eventName ~= "TIMER" then
            frame:RegisterEvent(eventName); -- Register all events for which handlers have been defined
        end;
    end;
end;

--[[
Registers a quest item.
Parameters:
    itemId (number)                 - the ID of an item to register.
    or questId (negative number)		- Negative IDs are used for quest that need a macro but do not
    											- have an item. The convention is to use -questId to get a unique id
    itemPriority (number)           - the priority of an item. The item with the highest priority will be used.
    usabliltyFunction (function)    - the function that is called to determine if the item is usable at the moment.
--]]
local function RegisterQuestItem(itemId, itemPriority, usabliltyFunction)
    QuestItemList[itemId] = {
        ItemPriority = itemPriority,
        IsItemUsable = usabliltyFunction
        };
end;

--[[
Returns a list of all registered quest items that are present in player's inventory.
The returned table has the following structure:
    result[itemId] = {ItemPriority = itemPriority, IsItemUsable = usabliltyFunction};
--]]
local result = {};
local function GetListOfAvailableQuestItems()
	wipe(result)

    for itemId, itemData in pairs(QuestItemList) do
        if (itemId and itemId < 0) or _G.GetItemCount(itemId) > 0 then
            result[itemId] = itemData;
        end;
    end;
   -- For debug
	DailyIntake.debugAvailQuestItems = result

    return result;
end;

--[[
Returns the best suitable quest item that may be used.
The result is an object of the following structure:
    result = {
        ItemId = itemId
        };
--]]
local function GetBestUsableQuestItem()
    local highestPriorityItemId = nil
    local alternativeMacroText = nil
    local isUsable = nil
    local macroIconIndex = nil
    local result = {
        ItemId = nil,
        AlternativeMacroText = nil,
        IconIndex = nil
        }

    for itemId, itemData in pairs(availableQuestItems) do
        isUsable, alternativeMacroText, macroIconIndex = itemData.IsItemUsable()
        if isUsable then
            if not highestPriorityItemId or itemData.ItemPriority > availableQuestItems[highestPriorityItemId].ItemPriority then
                highestPriorityItemId = itemId

                result.ItemId = itemId
                result.AlternativeMacroText = alternativeMacroText
                result.IconIndex = macroIconIndex
            end
        end
    end

    return result
end

--[[
Creates or modifies a macro that uses the best available quest item.
This function is called when player exits combat, changes target, changes the state
of inventory in any way, and every set amount of time.

Parameters:
    itemData - {ItemId = itemId}
--]]
local function ProvideMacro(itemData)
    -- return if player is in combat, because any macro modifiactions are
    -- not alowed in combat
    if _G.InCombatLockdown() then
		return;
	end;

	-- find macro
	local macroIndex = _G.GetMacroIndexByName("Daily");

	-- create macro if not present
	if macroIndex == 0 then
		macroIndex = _G.CreateMacro("Daily", 9, "", 1, 0)
	end

	-- set error message and error sound if there is no usable quest item
	local macroBody = "/script UIErrorsFrame:AddMessage(\"" .. DailyIntake.Localize("No suitable item found.") .. "\", 1.0, 0.0, 0.0, 53, 5);"
	    .. "PlaySoundFile(\"Sound\\\\Interface\\\\Error.wav\");";

	-- set macro to use the quest item
	local macroIconIndex = 9
	if itemData.ItemId ~= nil and (itemData.ItemId < 0 or _G.GetItemCount(itemData.ItemId) > 0) then
	    if itemData.AlternativeMacroText then
	        macroBody = itemData.AlternativeMacroText;
	    else
		    macroBody = "#showtooltip item:" .. itemData.ItemId .. "\n" .. "/use item:" .. itemData.ItemId
		end;
		macroIconIndex = itemData.IconIndex or 1
	end

	if itemData.IconIndex then
	    macroIconIndex = itemData.IconIndex;
	end;

	_G.EditMacro(macroIndex, "Daily", macroIconIndex, macroBody, 0)
end;

--[[
Refreshes the quest item macro to use the most appropriate item.
--]]
local function RefreshMacro()
    if isMacroRefreshEnabled then
        local itemData = GetBestUsableQuestItem();
        -- For debug
        DailyIntake.debugItemData = itemData
        ProvideMacro(itemData);
    end;
end;

local function EnableMacroRefresh()
   isMacroRefreshEnabled = true;
   availableQuestItems = GetListOfAvailableQuestItems();
	RefreshMacro();
end;

local function DisableMacroRefresh()
    isMacroRefreshEnabled = false;
end;


--[[
Returns true if the player is within specified range from specified coordinates,
nil otherwise.
--]]
local function PlayerIsCloseTo(x, y, range)
    local playerX, playerY = _G.GetPlayerMapPosition("player");

    if sqrt(abs(x - playerX)^2 + abs(y - playerY)^2) <= range then
        return true;
    end;
end;

--[[
Returns true if the player is within the specified rectangular area,
nil otherwise.
--]]
local function PlayerIsInArea(top, left, bottom, right)
    local playerX, playerY = _G.GetPlayerMapPosition("player");

    if playerX >= left and playerX <= right and playerY >= top and playerY <= bottom then
        return true;
    end;
end;

--[[
Returns true if the specified unit is the mob under given ID, nil otherwise.
The mob ID is the same as on sites such as WowHead.
--]]
local function UnitIsEntity(unitId, entityId, entityType)
    local guid = _G.UnitGUID(unitId);
    if guid == nil then return; end;

    local entityTypeStr, entityIdStr = guid:match("^0x(%x%x%x)%x(%x%x%x%x)");
    local entityTypeNum = tonumber("0x" .. entityTypeStr);
    local entityIdNum = tonumber("0x" .. entityIdStr);

    -- check if the unit is of required type
    if bit.band(entityTypeNum, 0x00F) ~= entityType then return false; end;

    -- check if the id of the mob matche the parameter
    if entityIdNum == entityId then
        return true;
    end;
end;

-- To find the GUID for a selected mob
local function DI_FindGUID(unitId)
	unitId = unitID or "target"
    local guid = _G.UnitGUID(unitId)
    if guid == nil then return end

    local entityTypeStr, entityIdStr = guid:match("^0x(%x%x%x)%x(%x%x%x%x)")
    local entityTypeNum = tonumber("0x" .. entityTypeStr) % 8
    local entityIdNum = tonumber("0x" .. entityIdStr)

	return 	entityIdNum, entityTypeNum, guid
end
_G.DI_FindGUID = DI_FindGUID


--[[
Return true if the unit has been tapped by other player, nil otherwise.
--]]
local function UnitIsTappedByOtherPlayer(unitId)
    if _G.UnitIsTapped(unitId) and (not _G.UnitIsTappedByPlayer(unitId)) then
        return true;
    end;
end;

--[[
Returns the index of the quest in the quest log.
questId - the ID of the quest as shown on web sites such as WowHead.
--]]
local function QuestIdToIndex(questId)
    for questIndex = 1, _G.GetNumQuestLogEntries() do
        local questLink = _G.GetQuestLink(questIndex);
        if questLink then
            if tonumber(questLink:match("\124c%x%x%x%x%x%x%x%x\124Hquest:(%d+)")) == questId then
                return questIndex;
            end;
        end;
    end;
end;

--[[
Use:
    local objectiveComplete, questNotTaken = QuestObjectiveIsDone(questId, objectiveIndex);

Parameters:
    questId             - the Id of the quest as shown on WowHead
    objectiveIndex      - the index of objective as shown in quest log (1 based)

Returns:
    objectiveComplete   - true if complete, nil otherwise
    questNotTaken       - true if player does not have the quest in his quest log, nil otherwise

--]]
local function QuestObjectiveIsDone(questId, objectiveIndex)
    local questIndex = QuestIdToIndex(questId);
    if not questIndex then return nil, true; end;

    local _, _, finished = _G.GetQuestLogLeaderBoard(objectiveIndex, questIndex);

    return finished;
end;

--[[
Use:
    local questComplete, questNotTaken = QuestIsDone(questId);

Parameters:
    questId             - the Id of the quest as shown on WowHead

Returns:
    questComplete       - true if complete, nil otherwise
    questNotTaken       - true if player does not have the quest in his quest log, nil otherwise

--]]
local function QuestIsDone(questId)
    local questIndex = QuestIdToIndex(questId);
    if not questIndex then return nil, true; end;

    -- get number of objectives
    local objectives = _G.GetNumQuestLeaderBoards(questIndex);

    for objectiveIndex = 1, objectives do
        local _, _, finished = _G.GetQuestLogLeaderBoard(objectiveIndex, questIndex);
        if not finished then return nil; end;
    end;

    return true;
end;

--[[
Use:
    local questComplete = QuestIsDoneOrNotTaken(questId);

Parameters:
    questId             - the Id of the quest as shown on WowHead

Returns:
    questComplete       - true if complete or if not in the quest log, nil otherwise

--]]
local function QuestIsDoneOrNotTaken(questId)
    local questIndex = QuestIdToIndex(questId)
    if not questIndex then return true end

    -- get number of objectives
    for objectiveIndex = 1, _G.GetNumQuestLeaderBoards(questIndex) do
        local _, _, finished = _G.GetQuestLogLeaderBoard(objectiveIndex, questIndex)
        if not finished then return nil end
    end;

    return true
end

--[[
Use:
    local questsComplete, questsNotTaken = QuestsAreDone(questId1, questId2, ...);

Parameters:
    questId             - the Id of the quest as shown on WowHead

Returns:
    questsComplete       - number of quests complete
    questsNotTaken       - number of quests not taken

--]]

local function QuestsAreDone(...)
    local questsComplete = 0;
    local questsNotTaken = 0;

    for i = 1, select("#", ...) do
        local questId = select(i, ...)

        local isComplete, isNotTaken = QuestIsDone(questId);
        if isComplete then
            questsComplete = questsComplete + 1;
        end;
        if isNotTaken then
            questsNotTaken = questsNotTaken + 1;
        end;
    end;

    return questsComplete, questsNotTaken;
end;

--[[
Use:
    local questsAreDone = QuestsAreDoneAndNotTaken(questId1, questId2, ...);

Parameters:
    questId             - the Id of the quests as shown on WowHead

Returns:
    questsAreDone			- true if all the quests are done or not present in the log,
    							  nil otherwise


--]]

local function QuestsAreDoneAndNotTaken(...)

	for i = 1,select('#', ...) do
		if not QuestIsDoneOrNotTaken(select(i,...)) then return nil end
	end
	--for _, questId in ipairs(...) do
	--	if not QuestIsDoneOrNotTaken(questId) then return nil end
	--end

	return true
end

--[[
Use:
    local questActive = QuestIsActive(questId1, questId2, ...);

Parameters:
    questId             - the Id of the quests as shown on WowHead

Returns:
    questActive			- true if at least one quests in the list is active i.e.
                          the quest is in the log and not completed,
                          nil if no quests in the list are active
--]]

local function QuestIsActive(...)

	for _, questId in ipairs({...}) do

		local questIndex = QuestIdToIndex(questId)
		if questIndex then
			 for objectiveIndex = 1, _G.GetNumQuestLeaderBoards(questIndex) do
				  if not select(3,_G.GetQuestLogLeaderBoard(objectiveIndex, questIndex)) then return true end
			 end;
		end

	end

	return nil

end


local function PlayerHasBuff(buffName)
    local name, _, _, _, _, _, _, _, _ =
        _G.UnitBuff("player", DailyIntake.Localize(buffName));

    if name ~= DailyIntake.Localize(buffName) then
        return;
    end;

    return true;
end;

local function PlayerHasDebuff(debuffName)
    local name, _, _, _, _, _, _, _, _ =
        _G.UnitDebuff("player", DailyIntake.Localize(debuffName));

    if name ~= DailyIntake.Localize(debuffName) then
        return;
    end;

    return true;
end;


-- player left combat
function events:PLAYER_REGEN_ENABLED()
    RefreshMacro();
end;

-- player's target changed
function events:PLAYER_TARGET_CHANGED()
    RefreshMacro();
end;

-- player's inventory changed
function events:BAG_UPDATE()
    availableQuestItems = GetListOfAvailableQuestItems();
    RefreshMacro();
end;

-- refresh on quest log update
function events:QUEST_LOG_UPDATE()
    availableQuestItems = GetListOfAvailableQuestItems();
    RefreshMacro();
end

-- timer handler
local elapsedTime = 0;
function events:TIMER(elapsed)
    if elapsedTime > DailyIntake.Settings.MacroRefreshInterval then
        RefreshMacro();

        elapsedTime = elapsedTime - DailyIntake.Settings.MacroRefreshInterval;
    end;

    elapsedTime = elapsedTime + elapsed;
end;

-- expose main functionality
DailyIntake.QuestItems = {
    Priority = Priority,
    Entity = Entity,
    Initialize = Initialize,
    RegisterQuestItem = RegisterQuestItem,
    PlayerIsCloseTo = PlayerIsCloseTo,
    PlayerIsInArea = PlayerIsInArea,
    UnitIsEntity = UnitIsEntity,
    UnitIsTappedByOtherPlayer = UnitIsTappedByOtherPlayer,
    QuestObjectiveIsDone = QuestObjectiveIsDone,
    QuestIsDone = QuestIsDone,
    QuestIsDoneOrNotTaken = QuestIsDoneOrNotTaken,
    QuestsAreDone = QuestsAreDone,
    QuestsAreDoneAndNotTaken = QuestsAreDoneAndNotTaken,
    QuestIsActive = QuestIsActive,
    PlayerHasBuff = PlayerHasBuff,
    PlayerHasDebuff = PlayerHasDebuff,
    EnableMacroRefresh = EnableMacroRefresh,
    DisableMacroRefresh = DisableMacroRefresh,
    RefreshMacro = RefreshMacro
    };

-- For debug
DailyIntake.QuestItemList = QuestItemList;
